import Scene from './scene'
import gameInfo from '../runtime/astray'
import util from '../base/util'
import rea from '../base/rea'
import explodeAnimation from '../base/explodeAnimation'

class system{
  constructor(parent, name){
    this.parent = parent
    this.name = name
  }
  async initializeRenderWorld(){
  }
  async updateRenderWorld(){
  }
  handleItemEvent2(type, x, y, index){
    
  }
  async handleItemEvent(type, x, y){
    return false
  }
}

class bombSystem extends system{
  constructor(parent){
    super(parent, "bomb")
    this.candidate = {}
    this.explosions = []
  }

  async initializeRenderWorld(){
    for (let i in this.parent.npcs){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name){
        let npc2 = await this.parent.__createRenderBall({map: 'images/astray/' + this.name + '.png'}, npc.x, npc.y, 0.2)
        npc2.type = npc.type
        this.parent.npcs[i] = npc2
      }
    }
  }
  async updateRenderWorld(){
    for (let i = this.parent.npcs.length - 1; i >= 0; --i){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name && npc.status && npc.status != 1){
          npc.type = ""
          this.explosions.push(new explodeAnimation(npc.status.x, npc.status.y, this.parent.THREE, this.parent.scene))
          this.parent.scene.remove(npc)
      }
    }
    let pCount = this.explosions.length
    while(pCount--) {
      this.explosions[pCount].update();
    }

    for (let i in this.candidate){
      let npc2 = await this.parent.__createRenderBall({map: 'images/astray/' + this.name + '.png'}, this.candidate[i].x, this.candidate[i].y, 0.2)
      npc2.type = this.name
      this.parent.npcs[i] = npc2
    }
    this.candidate = {}
  }

  handleItemEvent2(type, x, y, index){
    if (type == this.name){
      if (!this.candidate[index])
        this.candidate[index] = {x: x, y: y}
      return true
    }
    return false
  }

  async handleItemEvent(type, x, y){
    if (type == this.name){
      util.request2({type: "Play", openid: this.parent.openid, x: x, y: y, playtype: type})
      return true
    }
    return false
  }
}

class barriarSystem extends system{
  constructor(parent){
    super(parent, "barriar")
  }

  async initializeRenderWorld(){
    for (let i in this.parent.npcs){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name){
        let npc2 = await this.parent.__createRenderBall({color: "#00ffff"}, npc.x, npc.y, this.parent.npcRadius)
        npc2.type = npc.type
        this.parent.npcs[i] = npc2
      }
    }
  }

  async updateRenderWorld(){
    for (let i = this.parent.npcs.length - 1; i >= 0; --i){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name){
        this.parent.__rollBall(npc.x, npc.y, npc, npc.radius)
      }
    }
  }
}

class shieldSystem extends system{
  constructor(parent){
    super(parent, "shield")
    this.last_status = {}

    this.vertex_shader = `    
      precision lowp float;
      precision lowp int;
      uniform mat4 modelViewMatrix;
      uniform mat4 projectionMatrix;
      attribute vec3 position;
      varying vec3 vPosition;
      void main() {
          vPosition = position;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    `
    this.fragment_shader = `
      precision lowp float;
      precision lowp int;
      uniform float time;
      varying vec3 vPosition;
      void main(void){
        vec3 position = - 1.0 + 2.0 * vPosition;
        float red = abs(sin(position.x * position.y + time / 1.0));
        float green = abs(sin(position.x * position.y + time / 3.0));
        float blue = abs(sin(position.x * position.y + time / 6.0));
        gl_FragColor = vec4(red, green, blue, 1.0);
      }
    `
  }

  async updateRenderWorld(){
    for (let i in this.parent.players){
      let ply = this.parent.players[i]
      if (ply.status && ply.status[this.name] != undefined){
        let cur_status = ply.status[this.name] > 0
        if (this.last_status[i] != cur_status){
          if (ply.status[this.name]){
            ply.ballMesh.material = new this.parent.THREE.RawShaderMaterial({ 
              uniforms: {
                  time: {
                      value: ply.status[this.name]
                  }
              },
              vertexShader: this.vertex_shader,
              fragmentShader: this.fragment_shader, 
            })
          }else
            ply.ballMesh.material = new this.parent.THREE.MeshPhongMaterial({color: "#ff0000"})
          ply.ballMesh.material.needsUpdate = true
        }
        if (cur_status)
          ply.ballMesh.material.uniforms.time.value = ply.status[this.name]
        this.last_status[i] = cur_status
      }
    }
  }

  async handleItemEvent(type, x, y){
    if (type == this.name){
      util.request2({type: "Play", openid: this.parent.openid, x: x, y: y, playtype: type})
      return true
    }
    return false
  }
}

class smallSystem extends system{
  constructor(parent){
    super(parent, "small")
    this.last_status = {}
  }

  async updateRenderWorld(){
    for (let i in this.parent.players){
      let ply = this.parent.players[i]
      if (ply.status && ply.status[this.name] != undefined){
        if (this.last_status[i] != ply.status[this.name]){
          let r = ply.status[this.name]
          ply.ballMesh.scale.set(r / 0.2, r / 0.2, r / 0.2)
        }
        this.last_status[i] = ply.status[this.name]
      }
    }
    this.parent.players[this.parent.openid].ballMesh
  }

  async handleItemEvent(type, x, y){
    if (type == this.name){
      util.request2({type: "Play", openid: this.parent.openid, x: x, y: y, playtype: type})
      return true
    }
    return false
  }
}

export default class astrayRemoteScene extends Scene {
  constructor(aParent, aOpenID) {
    super(aParent, aOpenID)
    
    this.info = new gameInfo()

    this.bindTouchStart = this.__touchStart.bind(this)
    this.bindTouchMove = this.__touchMove.bind(this)
    this.bindTouchEnd = this.__touchEnd.bind(this)
    this.systems = [new barriarSystem(this), new bombSystem(this), new smallSystem(this), new shieldSystem(this)]
    
    rea.pipelines().add(aInput =>{
      if (this.parent.current_scene == "remote"){
        let inf2 = aInput.data()
        if (inf2.type == "playing"){
          if (this.gameState == 'play'){
            inf2.time = inf2.time - this.info.start_time
            this.frames.push(inf2)
            for (let i in this.npcs){
              if (this.npcs[i].type != "" && inf2.npcs[i].status){
                this.npcs[i].status = inf2.npcs[i].status
              }
            }
            for (let i in this.players)
              if (inf2.players[i].status)
                this.players[i].status = inf2.players[i].status
            for (let i = this.npcs.length; i < inf2.npcs.length; ++i){
              for (let j in this.systems){
                if (this.systems[j].handleItemEvent2(inf2.npcs[i].type, inf2.npcs[i].x, inf2.npcs[i].y, i))
                  break
              }
            }
          }
        }else if (inf2.type == "playerState")
          if (!inf2.playing)
            this.gameState = "error"
      }else
        aInput.out()
    }, {name: "fromServer"})

    rea.pipelines().find("socketStatusChanged").nextF(aInput => {
      if (aInput.data()){

      }else
        this.gameState = "error"
    })
  }

  __generate_maze_mesh(field, texture) {
    var dummy = new this.THREE.Geometry();
    for (var i = 0; i < this.mazeDimension; i++) {
        for (var j = 0; j < this.mazeDimension; j++) {
            if (field[i][j]) {
                var geometry = new this.THREE.CubeGeometry(1,1,1,1,1,1);
                var mesh_ij = new this.THREE.Mesh(geometry);
                mesh_ij.position.x = i;
                mesh_ij.position.y = j;
                mesh_ij.position.z = 0.5;
                mesh_ij.updateMatrix()
                dummy.merge(mesh_ij.geometry, mesh_ij.matrix)
            }
        }
    }
    var material = new this.THREE.MeshPhongMaterial({map: texture});
    var mesh = new this.THREE.Mesh(dummy, material)
    return mesh;
}

  async __createRenderBall(txt, x, y, radius){
    let g = new this.THREE.SphereGeometry(radius, 32, 16)
    if (txt.map){
      txt.map = await this.loader.load(txt.map)
    }
    //this.loader.load('images/astray/ball.png', t => {
      let m = new this.THREE.MeshPhongMaterial(txt)
      let ret = new this.THREE.Mesh(g, m)
      ret.position.set(x, y, radius)
      ret.rotation.y = - Math.PI / 2
      ret.x = x
      ret.y = y
      ret.radius = radius
      this.scene.add(ret)
    //})
    return ret
  }

  async __initializeRenderWorld(){
    // Add the light.
    this.light= new this.THREE.PointLight(0xffffff, 1)
    this.light.position.set(1, 1, 1.3)
    this.scene.add(this.light)
    
    // Add the ball.
    for (let i in this.players)
      this.players[i].ballMesh = await this.__createRenderBall({color: i == this.openid ? "#ff0000" : "#ffff00"}, this.players[i].x, this.players[i].y, this.ballRadius)
    for (let i in this.systems)
      await this.systems[i].initializeRenderWorld()
    //initialize maze
    let t0  = await this.loader.load('images/astray/brick.png')
    this.scene.add(this.__generate_maze_mesh(this.maze, t0))
    
    // Add the ground.
    let t1 = await this.loader.load('images/astray/concrete.png')
    let g = new this.THREE.PlaneGeometry(this.mazeDimension * 10, this.mazeDimension * 10, this.mazeDimension, this.mazeDimension)
    t1.wrapS = t1.wrapT = this.THREE.RepeatWrapping
    t1.repeat.set(this.mazeDimension * 5, this.mazeDimension * 5)
    let m = new this.THREE.MeshPhongMaterial({map:t1})
    let planeMesh = new this.THREE.Mesh(g, m)
    //planeMesh.position.set((this.mazeDimension - 1) / 2, (this.mazeDimension - 1) / 2, 0);
    //planeMesh.rotation.set(Math.PI/2, 0, 0);
    this.scene.add(planeMesh)

    // Add properties 
    let t2 = await this.loader.load('images/astray/coin.png')
    for (let i in this.prop_sets)
        if (this.prop_sets[i].type == "coin"){
          let pos = this.prop_sets[i].pos
          let c = new this.THREE.CylinderGeometry(0.25, 0.25, 0.1, 20, 1)
          let m = new this.THREE.MeshPhongMaterial({map: t2})
          //let t2_ = t2.clone()
          //t2_.needsUpdate = true
          //t2_.flipY = false
          let m2 = new this.THREE.MeshPhongMaterial({map: t2})
          let m0 = new this.THREE.MeshPhongMaterial({color: 0xffcc33})
          let coinMesh = new this.THREE.Mesh(c, [m0, m, m2])
          coinMesh.proptype = "coin"
          coinMesh.pos = pos
          this.prop_sets[i] = coinMesh
          coinMesh.position.set(pos.x, pos.y, 0.25)
          coinMesh.rotation.set(Math.PI / 2, Math.PI / 2, 0)
          this.scene.add(coinMesh)
        }
  }

__rotateMatrix(){
  let mx = arguments[0]
  for (let i = 1; i < arguments.length; i += 2)
    mx = new this.THREE.Matrix4().makeRotationAxis(arguments[i], arguments[i + 1]).multiply(mx)
  return mx
}


__rollBall(x, y, renderBall, radius){
  // Update ball position.
  var stepX = x - renderBall.position.x;
  var stepY = y - renderBall.position.y;

  renderBall.position.x = x;
  renderBall.position.y = y;

  // Update ball rotation.
  renderBall.matrix = this.__rotateMatrix(renderBall.matrix, 
    new this.THREE.Vector3(0,1,0), stepX / radius, 
    new this.THREE.Vector3(1,0,0), -stepY / radius)
  renderBall.rotation.setFromRotationMatrix(renderBall.matrix)
}

async __updateRenderWorld() {
  for (let i in this.players){
    let ply = this.players[i]
    this.__rollBall(ply.x, ply.y, ply.ballMesh, this.ballRadius)
  }
  for (let i in this.systems)
    await this.systems[i].updateRenderWorld()

  let player = this.players[this.openid].ballMesh
  // Update camera and light positions.
  let dx = (player.position.x - this.camera.position.x) * 0.1,
    dy = (player.position.y - this.camera.position.y) * 0.1
  this.camera.position.x += dx
  this.camera.position.y += dy
  this.camera.position.z += (5 - this.camera.position.z) * 0.1;
  this.light.position.x = this.camera.position.x;
  this.light.position.y = this.camera.position.y;
  this.light.position.z = this.camera.position.z - 3.7;
  this.info.updateScene(dx, dy)

  // Update properties 
  let x = Math.floor(player.position.x), y = Math.floor(player.position.y)
  let sx = Math.max(0, x - 2), ex = Math.min(this.mazeDimension - 1, x + 2),
    sy = Math.max(0, y - 3), ey = Math.min(this.mazeDimension - 1, y + 3)
  for (let i in this.prop_sets){
    let prop = this.prop_sets[i]
    if (prop && prop.proptype == "coin" && 
      prop.pos.x >= sx && prop.pos.x <= ex && 
      prop.pos.y >= sy && prop.pos.y <= ey){
        prop.matrix = this.__rotateMatrix(prop.matrix, new this.THREE.Vector3(0,1,0), 0.05)
        prop.rotation.setFromRotationMatrix(prop.matrix)
    }
  }
}

  /*__synchronizeRemote(){
    setTimeout(e=>{
      util.request({
        api: "astray/gameInfo",
        data: {openid: this.openid}
        }).then(e => {
          let inf2 = e.data
          inf2.delay = new Date().getTime() - inf2.time
          inf2.time = inf2.time - this.info.start_time
          this.frames.push(inf2)

          if (this.gameState != 'initialize' && this.gameState != 'fade out')
            this.__synchronizeRemote()
        })
    }, 30)
  }*/

  async __gameLoop(){
    switch(this.gameState) {      
      case 'initialize':
          this.info.initialize(this.scene, this.THREE, this.lv, this.openid, this.loader, this.camera)
          let inf = (await util.request({
                  api: "astray/gameInfo",
                  data: {openid: this.openid, 
                   first: true}
            })).data
          this.players = inf.players
          this.ballRadius = inf.ballRadius
          this.mazeDimension = inf.mazeDimension
          this.npcs = inf.npcs
          this.npcRadius = inf.npcRadius
          this.maze = inf.maze
          this.prop_sets = inf.prop_sets
          this.info.start_time = inf.time
          for (let i in this.info.items)
            await this.info.items[i].initRemoteCount(this.openid)

          await this.__initializeRenderWorld()
          this.camera.position.set(1, 1, 5)
          this.light.position.set(1, 1, 1.3)
          this.light.intensity = 0
          this.gameState = 'fade in'
          this.frames = []
          this.shows = []
          //this.__synchronizeRemote()
          break;
      case 'fade in':
          this.light.intensity += 0.1 * (1.0 - this.light.intensity);
          if (Math.abs(this.light.intensity - 1.0) < 0.05) {
              this.light.intensity = 1.0;
              this.gameState = 'play'
          }
          break
      case 'play':
          this.info.updateTime()
          let l = this.frames.length
          if (l >= 2){
            this.shows.push(this.info.current_time)
            let l2 = - 1
            for (let i = this.shows.length - 1; i >= 0; --i)
              if (this.shows[i] <= this.frames[l - 1].time && this.shows[i] >= this.frames[0].time){
                l2 = i
                break
              }
            if (l2 >= 0){
              let tn = this.shows[0]
              if (l2 > 10){
                tn = (this.shows[0] + this.shows[l2]) * 0.5
                this.shows.splice(0, l2)
                //console.log("hi")
              }else{
                this.shows.splice(0, 1) //this.shows[0] > this.frames[l - 1].time
              }
              //tn = Math.max(this.frames[0].time, tn)
              for (let i = l - 2; i >= 0; --i){
                if (this.frames[i].time <= tn){
                  //console.log(i)
                  var t0 = this.frames[i].time, t1 = this.frames[i + 1].time
                  for (let j in this.prop_sets)
                    if (this.prop_sets[j]){
                      this.prop_sets[j].type = this.frames[i].prop_sets[j].type
                      this.prop_sets[j].player = this.frames[i].prop_sets[j].player
                    }
                  if (i == l - 2){
                    var a = [(tn - t1) / (t0 - t1), (tn - t0) / (t1 - t0)]
                  }else{
                    let t2 = this.frames[i + 2].time
                    var a = [(tn - t1) * (tn - t2) / ((t0 - t1) * (t0 - t2)), (tn - t0) * (tn - t2) / ((t1 - t0) * (t1 - t2)), (tn - t0) * (tn - t1) / ((t2 - t0) * (t2 - t1))]
                  }
                  if (i > 0){
                    this.frames = this.frames.slice(i, l)
                  }
                  break
                }
              }
              if (a == undefined)
                console.log(this.frames[0].time + ";" + tn)
              for (let i in this.npcs){
                this.npcs[i].x = 0
                this.npcs[i].y = 0
              }
              for (let i in this.players){
                this.players[i].x = 0
                this.players[i].y = 0
              }
              for (let j = 0; j < a.length; ++j){
                for (let i in this.players){
                  this.players[i].x += this.frames[j].players[i].x * a[j]
                  this.players[i].y += this.frames[j].players[i].y * a[j]
                }
                for (let i in this.npcs)
                  if (this.frames[j].npcs[i]){
                    this.npcs[i].x += this.frames[j].npcs[i].x * a[j]
                    this.npcs[i].y += this.frames[j].npcs[i].y * a[j]
                  }
              }
              this.info.updateDelay(Math.round(this.info.current_time - tn))
            }else{
              this.info.updateDelay(Math.round(this.info.current_time - this.shows[0]))
            }
          }

          for (let i in this.prop_sets)
              if (this.prop_sets[i] && this.prop_sets[i].type == "none"){
                if (this.prop_sets[i].player == this.openid)
                  this.info.updateCoins()
                this.scene.remove(this.prop_sets[i])
                this.prop_sets[i] = null
              }
          await this.__updateRenderWorld()

          // Check for victory.
          let ply = this.players[this.openid].ballMesh
          var mazeX = Math.floor(ply.position.x + 0.5);
          var mazeY = Math.floor(ply.position.y + 0.5);
          if (mazeX == this.mazeDimension && mazeY == this.mazeDimension - 2) { 
              this.gameState = 'fade out';
          }
          break
  
      case 'fade out':
          this.light.intensity += 0.1 * (0.0 - this.light.intensity)
          if (Math.abs(this.light.intensity - 0.0) < 0.1) {
              await util.request({
                api: "astray/gameFinish",
                data: {openid: this.openid,
                success: true}
              })
              this.info.finish(this.mode)
              this.gameState = "finished"
              /*this.light.intensity = 0.0
              this.first_render = true
              this.gameState = 'initialize'*/
          }
          break 
      case 'error':
          this.info.updateDelay("断开", "red")
          break   
    }
  }

  __tryMove(x, y){
    let ct = {x: canvas.width / 2, y: canvas.height / 2}
    this.__setKeyAxis(x > ct.x ? 1 : x < ct.x ? - 1 : 0, y > ct.y ? - 1 : y < ct.y ? 1: 0)
  }

  async __touchStart(e){
    e.preventDefault()
    const x = e.touches[0].clientX
    const y = e.touches[0].clientY
    
    let ret = this.info.control(x, y)
    if (ret == "exit"){
      //this.gameState = "prefinished"
      util.request({
        api: "astray/gameFinish",
        data: {openid: this.openid}
      }).then(e => {
        this.gameState = "finished"
      })
    }/*else if (ret == "bomb"){
      util.request({
        api: "astray/updateItems",
        data: {openid: this.openid, 
        type: "bomb",
        count: - 1,
        cost: 0}
      })
    }*/else{
      let pos = this.players[this.openid].ballMesh.position
      for (let i in this.systems)
        if (await this.systems[i].handleItemEvent(ret, pos.x, pos.y))
          return
      this.__tryMove(x, y)
    }
  }

  __setKeyAxis(x, y){
    util.request2({type: "Play", openid: this.openid, x: x, y: y})
    /*util.request({
      api: "astray/playOnce",
      data: {openid: this.openid, 
             x: x, 
             y: y}})
    .then(res => {
        //console.log(res)
    })*/
  }

  __touchEnd(e){
    e.preventDefault()
    this.__setKeyAxis(0, 0)
  }

  __touchMove(e){
    e.preventDefault()
    const x = e.touches[0].clientX
    const y = e.touches[0].clientY
    this.__tryMove(x, y)
  }

  update(){
    //combined to gameloop
    if (this.gameState == "finished"){
     // this.gameState = "fade out"
      return {scene: "loading2"}
    }
    else
      return Scene.prototype.update.apply(this, arguments)
  }
  async render(ctx){
    await ctx.update(async (THREE, scene, loader, camera) => {
      if (this.first_render){
        Scene.prototype.render.apply(this, arguments)
        this.THREE = THREE
        this.scene = scene
        this.loader = loader
        this.camera = camera
        this.gameState = 'initialize'
      }
      await this.__gameLoop()
    })
  }

  enter(aInfo){
    /*this.first_render = false
    this.gameState = "???"
    util.request({
      api: "astray/listrecords",
      data: {player: this.openid, 
             type: 'single'}})
    .then(res => {
        let rec = res.data
        rec.sort((p, n) => {
          return n.stage - p.stage
        })
        if (rec.length)
          this.mazeDimension = (rec[0].stage + 5) * 2 + 1
        else
          this.mazeDimension = 11
    })*/
    this.lv = aInfo.level
    this.mode = aInfo.mode
    this.gameState = "???"
    Scene.prototype.enter.apply(this, arguments)
    canvas.addEventListener('touchstart', this.bindTouchStart)
    canvas.addEventListener('touchmove', this.bindTouchMove)
    canvas.addEventListener('touchend', this.bindTouchEnd)
  }

  leave(){
    canvas.removeEventListener('touchstart', this.bindTouchStart)
    canvas.removeEventListener('touchmove', this.bindTouchMove)
    canvas.removeEventListener('touchend', this.bindTouchEnd)
  }
}